package trab_sd;

import java.net.*;
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import sun.net.ConnectionResetException;

public class User implements Serializable, Runnable
{
	private UserID id;
	private UserID serverId;
	private ServerSocket connection;
	private File directory;
	private int numberOfConnections;
	private FileList fileList;
	private UserList users;
	private ExecutorService threadExecutor;
	private boolean cont;
	
	public User( UserID id, UserID serverId, String path ) throws Exception
	{
		cont = true;
		
		this.id = id;
		this.serverId = serverId;
		
		
		//estabelece as conexoes
		connection = new ServerSocket( id.getPort() );
		
		//System.out.println("Criado socket de recepcao de requisicoes");
		
		fileList = new FileList();
		
		//abre o diretorio
		directory = new File( path );
		if ( !directory.isDirectory() )
			throw new Exception("Não é um diretorio");
		String [] dir = directory.list();
		
		//cria a lista de arquivos com os arquivos locais
		for ( String arq : dir )
		{
			FileListNode tmp = new FileListNode(arq);
			tmp.addUser(id);
			fileList.add(tmp);
		}
		
		//cria list ade usuarios
		users = new UserList();
		users.add(id);
		users.add(serverId);
		
		//cria um executor de threads
		threadExecutor = Executors.newCachedThreadPool();
		
		//inicia o gerente de requisicoes
		threadExecutor.execute(this);
		threadExecutor.execute(new WatchDir( Paths.get(directory.getAbsolutePath()), false, this));
		//System.out.println("Rodando listener");
		
		Socket serverConnection = new Socket( serverId.getName(), serverId.getPort() );
		//System.out.println("Conectado com o servidor");
		
		
		ObjectOutputStream out = new ObjectOutputStream( serverConnection.getOutputStream() );
		out.flush();
		//System.out.println("Criado ObjectOutputStream");
		
		
		ObjectInputStream in = new ObjectInputStream( serverConnection.getInputStream() );
		//System.out.println("Criado ObjectInputStream");
		
		String answer = "YOLOLO";
		int i = 0;
		//System.out.println("ID :" + id);
		//System.out.println("SERVER: " + serverId);
		//System.out.println("EH SERVIDOR?" + isServer());
		//if ( !isServer() ) {
		try {
			//System.out.println("Enviando Lista de usuarios");
		
			//manda lista de usuario
			while ( !answer.equals("OK") && i < 3 ) {
				out.writeUTF("SENDING_USER_LIST");
				out.flush();
				///System.out.println("Enviado a mensagem de requisicao");
				answer = in.readUTF();
				out.writeObject(this.users);
				out.flush();
				out.writeUTF("FINISHED");
				out.flush();
				//System.out.println("A resposta a requisicao foi:" + answer);
				i++;
			}
			if (i == 3) {
				throw new Exception("Sem resposta coerente do no");
			}
			serverConnection.close();
			out.close();
			in.close();
			
			serverConnection = new Socket( serverId.getName(), serverId.getPort() );
			out = new ObjectOutputStream( serverConnection.getOutputStream() );
			out.flush();
			in = new ObjectInputStream( serverConnection.getInputStream() );
			i = 0;
			answer = "NotOK";
			//System.out.println("Enviando arquivos");	
			while ( !answer.equals("OK") && i < 3 ) {
				out.writeUTF("SENDING_FILE_LIST");
				out.flush();
				answer = in.readUTF();
				out.writeObject(this.fileList);
				out.flush();
				out.writeUTF("FINISHED");
				out.flush();
			}
			out.close();
			in.close();
			if (i == 3) {
				throw new Exception("Sem resposta coerente do no");
			}
		}
		catch (SocketTimeoutException ste)
		{
			ste.printStackTrace();
		}
		//}
	}

//##############################################################################
	//Funcao que recebe conexoes e toma as acoes correspondentes
	public void run()
	{
		while (true)
		{
			try
			{
				//aceita conexao
				Socket server = connection.accept();
				
				//cria streams de comunicacao
				ObjectOutputStream out = new ObjectOutputStream( server.getOutputStream() );
				ObjectInputStream in = new ObjectInputStream( server.getInputStream());
				
				//recebe a requisição
				String ans;
				out.flush();
				ans = in.readUTF();
				
				//verifica qual o tipo da requisicao e qual a acao correspondente
				if ( ans.equals("SENDING_FILE_LIST") ) {
					threadExecutor.execute( new FileListReceiver( this, server, out, in ) );
				
				}
				else if ( ans.equals("SENDING_USER_LIST") ) {
					//System.out.println("Recebendo lista de usuarios");
					threadExecutor.execute( new UserListReceiver( this, server, out, in ) );
				}
				else if ( ans.equals("SENDING_FILE")) {
					String fname = in.readUTF();
					long fsize = in.readLong();
					threadExecutor.execute(new FileReceiver( directory+ "/", fname, fsize, out, in, server,this));
					
				}
				else if ( ans.equals("GET_FILE") )  {
					String fname = in.readUTF();
					UserID dst = (UserID) in.readObject();
					String serverIp = server.getInetAddress().getHostAddress();
					server.close();
					in.close();
					out.close();
					threadExecutor.execute( new FileSender(this, directory + "/", fname, dst) );
				}
				else if ( ans.equals ("REMOVE_FILE") ) {
					String fname = in.readUTF();
					UserID uId	= (UserID) in.readObject();
					//out.writeUTF("OK");
					//out.flush();
					removeUserFile(uId, fname);
					System.out.println("Removido o arquivo do usuario.");
					out.writeUTF("FINISHED");
					//System.out.println(fileList);
					if ( isServer() ) {
						sendRemoveMsgToAll(uId, fname);
					}
					updateFileList( fileList );
				}
				else if ( ans.equals("OFFLINE_USER")) {
					UserID tmp = (UserID) in.readObject();
					removeOfflineUser(tmp);
				}
			}
			catch (Exception e)
			{
				e.printStackTrace();
			}
		}
	}

//###################### Funcoes de manipulacao de lista #######################
	public synchronized void updateUserList(UserList ul) {
		UserList lastList = users;
		setUserList(ul);
		Iterator itr = lastList.iterator();
		UserID tmp;
		
		users.add(id);
		
		if (isServer())
		for (UserID u : lastList ) {
			users.add(u);
		/*for (UserID u : users ) {
			if (u.equals(id))
				continue;
			try {
				Socket s = new Socket(u.getName(), u.getPort());
				s.close();
			}
			catch (Exception e) {
				users.removeByIP(u.getIP());
				fileList.removeUser(u);
			}
		}*/
		}
		/*while (itr.hasNext())
		{
			tmp = (UserID) itr.next();
			users.add(tmp);
		}
		itr = getUserList().iterator();
		while (itr.hasNext()) {
			tmp = (UserID) itr.next();
			if (tmp.equals(id))
				continue;
			try {
				Socket s = new Socket(tmp.getName(), tmp.getPort());
				s.close();
			}
			catch (Exception e) {
				users.removeByIP(tmp.getIP());
				fileList.removeUser(tmp);
			}
		}*/
	}
	
	public synchronized void updateFileList(FileList fl) {
		FileList lastList = fileList;
		fileList = fl;
		
		Iterator itr = lastList.iterator();
		FileListNode tmp;
		System.out.println("Nova lista de arquivos recebida.");
		while (itr.hasNext())
		{
			tmp = (FileListNode) itr.next();
			fileList.add(tmp);
			//System.out.println(tmp.getName());
		}
	}

	public synchronized void removeUserFile( UserID uid, String fname ) {
		fileList.removeUserFromFile(fname, uid);
	}
	
	public void removeOfflineUser(UserID u) {
		System.out.println("User: usuario " + u.getName() + " is Offline");
		users.remove(u);
		System.out.println(u);
		System.out.println(users);
		fileList.removeUser(u);
		if (u.equals(serverId)) {
			serverId = users.getFirst();
			System.out.println("New server is: " + serverId);
			if (!isServer())
			try {
				threadExecutor.execute(new UserListSender(this, serverId));
				threadExecutor.execute(new FileListSender(this, serverId));
			}
			catch (Exception e) {
				
			}
		}
		else if (isServer() ) {
			sendOfflineMsgToAll(u);
		}
	}
	
//##################### Funcoes de passagem de mensagem ########################
	public void sendUserListToAll() {
		Iterator itr = users.iterator();
		while ( itr.hasNext() ) {
			UserID tmp = (UserID) itr.next();
			if ( tmp.equals( id ) )
				continue;
			
			threadExecutor.execute( new UserListSender( this, tmp ));	
		}
	}
	
	public void sendFileListToAll() {
		Iterator itr = users.iterator();
		while ( itr.hasNext() ) {
			UserID tmp = (UserID) itr.next();
			if ( tmp.equals( id ) )
				continue;
			threadExecutor.execute( new FileListSender( this, tmp ));	
		}
	}
		
	public void sendRemoveMsgToAll( UserID fowner, String fname) {
		Iterator itr = users.iterator();
		while ( itr.hasNext() ) {
			UserID tmp = (UserID) itr.next();
			if ( tmp.equals( id ) ) 
				continue;
		System.out.println("Mandando mensagem para" + tmp.getName());
		threadExecutor.execute(new FileListRemove(tmp, fowner, fname, this) );
		}
	}
	
	public void sendOfflineMsgToAll( UserID offUser ) {
		Iterator itr = getUserList().iterator();
		while ( itr.hasNext() ) {
			UserID tmp = (UserID) itr.next();
			if ( tmp.equals( getID() ) || tmp.equals(offUser) )
				continue;
			threadExecutor.execute(new OfflineUserMsg(offUser, tmp));
		}
	}
	
	public void threadPoolExecute(Runnable r) {
		threadExecutor.execute(r);
	}
	
	//#################### Getters e setters ###################################
	
	public UserID getID() {
		return id;
	}
	
	public UserID getServerID() {
		return serverId;
	}
	
	public synchronized UserList getUserList() {
		return users;
	}
	
	public FileList getFileList() {
		return fileList;
	}
	
	public String getDirectory(){
		return directory.getAbsolutePath();
	}
	
	public synchronized void setUserList( UserList u ) {
		users = u;
	}
	
	public void setFileList( FileList f ) {
		fileList = f;
	}
	
	public void setCont( boolean b ) {
		cont = b;
	}
	
	public boolean isServer() {
		return id.equals(serverId);
	}
	
//####################### Interface com o Usuario ##############################
	public void menu() {
		System.out.println("Digite \"lf\" para visualizar os arquivos disponiveis e \"lu\" para os usuarios.");
		System.out.println("Para baixar um arquivo, digite o seu nome.");
		while (true) {
			Scanner scanIn = new Scanner(System.in);
			String s = scanIn.nextLine();
			//pergunta o nome do arquivo desejado
			
			if (s.equals("lf")) {
				if ( !isServer() ) {
					//cont = false;
					UserListSender fls = new UserListSender(this, serverId);
					long i = 0;
					fls.run();
				}
				System.out.println(fileList);
				continue;
			}
			if (s.equals("lu")) {
				if ( !isServer() ) {
					UserListSender fls = new UserListSender(this, serverId);
					fls.run();
				}
				System.out.println(users);
				continue;
			}
			
			FileListNode tmp = fileList.getByName(s);
			if (tmp == null) {
				System.out.println("Arquivo nao encontrado.");
				continue;
			}
			UserID tmpId = tmp.getUser();
			//envia requisicao ao dono do arquivo
			try {
				//Conecta-se ao cliente que possui o arquivo
				Socket con = new Socket( tmpId.getName(), tmpId.getPort() );
				con.setSoTimeout(50000);
				//Cria streams
				ObjectOutputStream conOut = new ObjectOutputStream(con.getOutputStream());
				ObjectInputStream conIn = new ObjectInputStream(con.getInputStream());
				//Faz requisicao de arquivo
				conOut.writeUTF("GET_FILE");
				conOut.flush();
				conOut.writeUTF(tmp.getName());
				conOut.flush();
				conOut.writeObject(id);
				conOut.flush();
				System.out.println("Baixando item ...");
		
			}
			catch ( SocketTimeoutException ste ) {
				System.out.println("Nao houve resposta.");
				ste.printStackTrace();
			}
			catch ( Exception e ) {
				//e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		try {
			//Cria IDs de usuario e de servidor
			UserID me = new UserID( InetAddress.getLocalHost().getHostAddress(), InetAddress.getLocalHost().getHostName(), Integer.parseInt(args[2]) );
			UserID serverID = new UserID (InetAddress.getByName(args[0]).getHostAddress(), InetAddress.getByName(args[0]).getHostName(), Integer.parseInt( args[1]) );
			User i = new User ( me, serverID, args[3]);
			i.menu();
		}
		catch( Exception e )
		{
			e.printStackTrace();
		}
	}
}